package ar.com.jsejtman.common.persistence.dao;

import ar.com.jsejtman.common.service.data.DataPage;
import ar.com.jsejtman.common.service.data.SortCriterion;
import ar.com.jsejtman.common.service.data.SortDirection;
import java.util.List;
import org.hibernate.Criteria;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Projections;

/**
 * Criteria de Hibernate con manejo de paginación.
 * 
 * @param <Entity> Entidad involucrada.
 * @author jsejtman
 */
public class PaginatedCriteria<Entity extends Object> {

	/**
	 * Criteria de Hibernate.
	 */
	private Criteria criteria;

	/**
	 * Número de página a obtener.
	 */
	private Integer pageNumber;

	/**
	 * Tamaño de página a obtener.
	 */
	private Integer pageSize;

	/**
	 * Criterio de ordenamiento (si corresponde).
	 */
	private SortCriterion sortCriterion;

	/**
	 * Asigna el Criteria de Hibernate a usar.
	 * 
	 * @param criteria Criteria de Hibernate a usar.
	 */
	public final void setCriteria(final Criteria criteria) {

		this.criteria = criteria;
	}

	/**
	 * Asigna el número de página a obtener (1 a N).
	 * 
	 * @param pageNumber Número de página a obtener.
	 */
	public final void setPageNumber(final Integer pageNumber) {

		this.pageNumber = pageNumber;
	}

	/**
	 * Asigna el tamaño de página a obtener (1 a N).
	 * 
	 * @param pageSize Tamaño de página a obtener.
	 */
	public final void setPageSize(final Integer pageSize) {

		this.pageSize = pageSize;
	}

	/**
	 * Asigna el criterio de ordenamiento a usar (si corresponde).
	 * 
	 * @param sortCriterion Criterio de ordenamiento a utilizar.
	 */
	public final void setSortCriterion(final SortCriterion sortCriterion) {

		this.sortCriterion = sortCriterion;
	}

	/**
	 * Obtiene una página de datos a partir de un Criteria de Hibernate, y un número y tamaño de
	 * página preconfigurados.
	 * 
	 * @return Página de datos obtenida.
	 */
	public final DataPage<Entity> getDataPage() {

		this.checkRequiredFields();

		/*
		 * Obtiene la cantidad total de resultados: de esta manera, es posible detectar si pide una
		 * posición de resultados fuera del rango de los disponibles.
		 */
		final Long totalResultsFound =
		        (Long) this.criteria.setProjection(
		                Projections.projectionList().add(Projections.rowCount())).uniqueResult();

		this.criteria.setProjection(null);
		this.criteria.setResultTransformer(Criteria.ROOT_ENTITY);

		/*
		 * Se determina el offset del registro a partir del cual obtener (el offset es exclusive: es
		 * decir, se obtienen registros a partir del inmediato siguiente al offset).
		 */
		Integer offset = (this.pageNumber - 1) * this.pageSize;
		Integer currentPageNumber = this.pageNumber;

		if (offset >= totalResultsFound.intValue()) {
			/* Resultado fuera de rango: procede a obtener la primera página de resultados */
			offset = 0;

			currentPageNumber = 1;
		}

		/* Se elabora el criterio de ordenamiento, si corresponde */
		if (this.sortCriterion != null) {

			if (this.sortCriterion.getFieldName() != null
			        && this.sortCriterion.getDirection() != null) {

				if (this.sortCriterion.getDirection() == SortDirection.ASCENDING) {
					this.criteria.addOrder(Order.asc(this.sortCriterion.getFieldName()));
				} else {
					this.criteria.addOrder(Order.desc(this.sortCriterion.getFieldName()));
				}
			} else {
				throw new IllegalArgumentException("El criterio de ordenamiento está incompleto.");
			}
		}

		this.criteria.setFirstResult(offset);
		this.criteria.setMaxResults(this.pageSize);

		@SuppressWarnings("unchecked")
		final List<Entity> resultsFound = this.criteria.list();

		final DataPage<Entity> dataPageFound = new DataPage<Entity>();
		dataPageFound.setPage(currentPageNumber);
		dataPageFound.setRows(resultsFound);
		dataPageFound.setLimit(this.pageSize);
		dataPageFound.setRecords(totalResultsFound.intValue());

		return (dataPageFound);
	}

	/**
	 * Controla que todos los campos obligatorios, así como los valores de los mismos, sean
	 * correctos, antes de realizar una operación. En caso de que no, lanzará una excepción
	 * indicando el problema.
	 */
	private void checkRequiredFields() {

		if (this.criteria == null || this.pageNumber == null || this.pageSize == null) {
			throw new IllegalArgumentException("El criterio a paginar, el número de página, "
			        + "y el tamaño de página, son obligatorios.");
		}

		if (this.pageNumber < 1) {
			throw new IllegalArgumentException("El número de página debe ser mayor o igual a 1.");
		}

		if (this.pageSize < 1) {
			throw new IllegalArgumentException("El tamaño de página debe ser mayor o igual a 1.");
		}
	}
}
